# ===============================================================================================================
#   ReallyPreventStandby - a plugin for Squeezebox Server 7.5 and greater.  Fork of the stock
#                          Logitech SBS PreventStandby plugin.
#
#   Prevents the system (Windows, Linux/Gnome, OSX) from sleeping while music is playing.
#
#   Version 20110307.102347
#
#   Copyright (C) 2010 Gordon Harris
#
#   Plugin.pm -- adapted from /Slim/plugin/PreventStandby/Plugin.pm
#
#	Changelog
#
#   1.0 - 2006-04-05 - Initial Release of original PreventStandby plugin by Julian Neil
#
#   2.0 - 2009-01-03 - Proposed changes by Gordon Harris:
#                      Added "nIdleTime" feature -- waits at least $nIdleTime number
#                      of idle player intervals before allowing standby.  Also, is
#                      "resume aware" -- resets the idle counter on system resume
#                      from standby or hibernation.
#         2009-01-10 - Changed to a user plugin until such time as these
#                      or other proposed chages to PreventStandby are adopted by
#                      the SqueezeCenter developers.
#         2010-11-16 - Extended plugin to work with OSX and linux+gnome
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.
#

package Plugins::ReallyPreventStandby::Plugin;

use strict;
use Slim::Utils::Log;
use Slim::Utils::Prefs;
use Slim::Utils::OSDetect;
#use File::Spec::Functions qw(:ALL);
use Cwd qw(abs_path);


if ($^O =~ /^m?s?win/i) {		## Are we running windows?
	eval {
	require Win32::API;
	import Win32::API;
	} ;
}


if ( main::WEBUI ) {
	require Plugins::ReallyPreventStandby::Settings;
}

# Constants..
# How many seconds between checks for busy clients..
# Reduce this value when testing, unless you are very patient.
use constant INTERVAL => 60;

# Global Variables..
# Change this to a 'our' declaration?

use vars qw(%g);

#our %g = (
%g = (
	nAppVersion			=> 20110307.102347,								#version of this code..
	nSCVersion			=> Plugins::ReallyPreventStandby::Settings::GetSCVersion(),
	szOS				=> Slim::Utils::OSDetect::OS(),
	szLogfile			=> Slim::Utils::OSDetect::dirsFor('log') . '/reallypreventstandby.log',
	nLastCheckTime		=> time(),
	nHasBeenIdle		=> 0,
	bIsInhibited		=> 0,
	bHasRequestedWakeup => 0,
	log					=> Slim::Utils::Log->addLogCategory({
							'category'     => 'plugin.ReallyPreventStandby',
							'defaultLevel' => 'ERROR',
							'description'  => 'PLUGIN_REALLYPREVENTSTANDBY', }),							#log hash
	prefs				=> preferences('plugin.reallypreventstandby'),										#preferences
	prefDefaults		=> undef,
);

sub initPrefDefaults {
	#my $szOS = Slim::Utils::OSDetect::OS();

	my $prefDefaults = {	'nPrefsVersion'					=>  0,
							'nIdleTime'						=>	20,
							'bCheckPower'					=>	0,
							'bInhibitIsToggle'				=>	0,
							'szInhibitCmd'					=>	"",
							'szUninhibitCmd'				=>	"",
					   };

	my $szInhibitApp = GetPluginPath();
	$szInhibitApp .= '/bin/' . $g{szOS} . '/';
	
	for ($g{szOS}) {
		if (/win/) {
		#do nothing...we don't use an external program to inhibit on windows..
		;;
			
		} elsif (/unix/) {
			$szInhibitApp .= 'gsession-inhibit';
	
			if ( $g{log}->is_debug ) {
				$prefDefaults->{szInhibitCmd} = "sudo -u %u $szInhibitApp %t --log %l --debug";
				
			} else {
				$prefDefaults->{szInhibitCmd} = "sudo -u %u $szInhibitApp %t --quiet";
			}
		} elsif (/mac/) {
			$szInhibitApp .= 'sleep-inhibit';
			if ( $g{log}->is_debug ) {
				$prefDefaults->{szInhibitCmd} = "$szInhibitApp %t --log %l --debug";
				
			} else {
				$prefDefaults->{szInhibitCmd} = "$szInhibitApp %t --quiet";
			}
		}
	}
	
	$g{log}->is_debug && $g{log}->debug("szInhibitCmd == $prefDefaults->{szInhibitCmd}");
	
	return $prefDefaults;
}


sub GetPluginPath {
	my $fullname = File::Spec->rel2abs( __FILE__ );
	my ($volume, $path, $file) = File::Spec->splitpath($fullname);

	#Another aproach to resolve symlinks..
    my $absolute_path = $volume ? $volume . $path : $path;
    $path = abs_path($absolute_path);

	return $path;
}


my $hRecursion = 0;

sub logHashElements {

	my @myhash = shift;
	my $num = 0;
	$hRecursion++;

	if (! $g{log}->is_debug ) {
		return;
	}

	if (!defined(@myhash)) {
		$myhash[0] = 'undefined';
	}

	foreach my $item (@myhash) {
		$g{log}->debug("item\[$num\]:");

		if ( ref($item) ne 'HASH' ) {
			$g{log}->debug("$item is not a HASH.");
		} else {
			while( my ($k, $v) = each %$item ) {
				$g{log}->debug("key: $k, value: " . ( defined($v) ? $v : "undef" ) );
				#recursion limited to 10 levels deep..
				if ( defined($v) && ( ref($v) eq "HASH" ) && ( $hRecursion < 10 ) ) {
					logHashElements($v);
				}
			}
		}
		$num++;
	}
	$hRecursion--;
}


sub setPrefValidations {
	$g{prefs}->setValidate({ 'validator' => 'intlimit', 'low' => 0, 'high' => 240 }, 'nIdleTime');
}


sub prefsChange {
	# Reset our counter on prefs change..
	$g{nHasBeenIdle} = 0;
	Plugins::ReallyPreventStandby::Settings::readPrefs();
}


sub getDisplayName {
	return 'PLUGIN_REALLYPREVENTSTANDBY';
}


sub initPlugin {
	my ($class) = @_;
	
	$g{prefDefaults} = initPrefDefaults();
	
	if ( main::WEBUI ) {
		Plugins::ReallyPreventStandby::Settings->new($class);
	}

	if (my $nIdleTime = $g{prefs}->get('nIdleTime')) {
		$g{log}->is_debug && $g{log}->debug("System standby now allowed after $nIdleTime minutes of player idle time.");
	}
	else {
		$g{log}->is_debug && $g{log}->debug("System standby now inhibited..");
	}
	
	# Register our CLI command
	Slim::Control::Request::addDispatch(['reallypreventstandby','_request'], [0, 0, 0, \&_pluginCLI]);
	

	$g{log}->is_debug && $g{log}->debug("Plugin " . Slim::Utils::Strings::string('PLUGIN_REALLYPREVENTSTANDBY') . " initialized for $g{szOS}..");

	checkClientActivity();
}


sub _inhibitWindows {
	my $szCommand = $g{prefs}->{szInhibitCmd};
	
	if ($szCommand) {
		$szCommand = _fixupInhibitCommand( $szCommand );
		
		$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");
		my $res = system $szCommand;
		$g{log}->is_debug && $g{log}->debug("system call returned $res");

		$g{bIsInhibited} = ($res == 0);

		
	} elsif ($^O =~ /^m?s?win/i) {		

		my $SetThreadExecutionState = Win32::API->new('kernel32', 'SetThreadExecutionState', 'N', 'N');

		if (defined $SetThreadExecutionState) {
			main::INFOLOG && $g{log}->is_info && $g{log}->info("Preventing Windows System Standby via SetThreadExecutionState..");
			$SetThreadExecutionState->Call(1);
			$g{bIsInhibited} = 1;
		} else {
			return 0;
		}
	}
	
	return 1;
}

sub _uninhibitWindows {
	my $szCommand = $g{prefs}->{szUninhibitCmd};

	if ($szCommand) {
		$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");
		my $res = system $szCommand;
		$g{log}->is_debug && $g{log}->debug("system call returned $res");
		$g{bIsInhibited} = ($res != 0);
	}
	return 1;
}



sub _killInhibitCmd {
	my $szKillCmd;
	my $szCommand = $g{prefs}->{szInhibitCmd};

	my ($volume,$directories,$file) = File::Spec->splitpath( $szCommand );
	
	my $n = index($file, ' ');
	if ($n != -1){
		$file = substr($file, 0, $n)
	}
	 
	if ($g{szOS} eq 'unix') {
		my ($szGnomeUser, $szDisplay) = _getGnomeUser();
		$szKillCmd = "sudo -u $szGnomeUser killall -9 ";
	} else {
		$szKillCmd = 'killall -9 ';
	}

	$szKillCmd .= $file;
	
	$g{log}->is_debug && $g{log}->debug( "Attempting: $szKillCmd" );
	
	my $res = system $szKillCmd;

	$g{log}->is_debug && $g{log}->debug("system call returned $res");

}

# who
#daadmin  tty7         2010-11-23 13:01 (:0)
#daadmin  pts/0        2010-11-23 13:01 (:0.0)
#daadmin  pts/1        2010-11-23 13:40 (:0.0)
#gordon   tty8         2010-11-23 14:02 (:1)
#daadmin  pts/2        2010-11-23 14:02 (:0.0)

sub _getGnomeUser {
	my $szUsers;
	my $szGnomeUser;
	my $szDisplay;
	
	$szUsers = `/usr/bin/who`;

	#username  tty7         2010-11-23 13:01 (:0)
	if ( $szUsers =~ m/^(\w+)\s.*\((\:\d)\)$/mg ) {
		$szGnomeUser = $1;
		$szDisplay = $2;
	} else {
		$szGnomeUser = undef;
		$szDisplay = undef;
	}
	
	$g{log}->is_debug && $g{log}->debug( "Gnome User == " . (defined($szGnomeUser) ? $szGnomeUser : "Unknown") . " Display == " . (defined($szDisplay) ? $szDisplay : "Unknown"));
	
	return ($szGnomeUser, $szDisplay);
}


sub _fixupInhibitCommand {
	my ($szCommand, $szGnomeUser, $szDisplay) = @_;
	
	if (!defined($szCommand)) {
		return "";
	}

	my $nInhibtTime = INTERVAL;

	
	#$g{log}->debug( "command before: $szCommand" );

	$szCommand =~ s/%t/$nInhibtTime/g;		# Fixup the time var..
	
	$szCommand =~ s/%l/$g{szLogfile}/g;		# Fixup the log var..
	
	if ( defined ($szGnomeUser) ) {
		$szCommand =~ s/%u/$szGnomeUser/g;		# Fixup the user var..
	}

	#If our user isn't on display device :0.0, the append a display override..
	if (defined($szDisplay)) {
		if ( ! ($szCommand =~ s/%d/$szDisplay/g) && $szDisplay ne ":0") {
			$szCommand = $szCommand . " --display $szDisplay";
		}
	}

	#$g{log}->debug( " command after: $szCommand" );
	
	return $szCommand;
}


sub _inhibitGnome {
	my $szCommand = $g{prefs}->{szInhibitCmd};
	
	if (!$szCommand) {
		return 0;
	}
	
	my ($szGnomeUser, $szDisplay) = _getGnomeUser();

	#if no gnome user and if we need one for the command..
	if ( !defined($szGnomeUser) && index($szCommand, "%u") != -1 ) {
		$g{log}->error( "Could not determine Gnome user!");
		return 0;
	}
	
	$szCommand = _fixupInhibitCommand( $szCommand, $szGnomeUser, $szDisplay );
	#$g{log}->debug( "szCommand == $szCommand");
	
	#redirect output and make it so the command returns immediately..
	$szCommand = $szCommand . " >/dev/null 2>&1 &";
	#$g{log}->debug( "szCommand == $szCommand");

	#execute the system inhibit utility..
	$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");
	
	#Pass a string, not an array, so that perl will execute the command with /bin/sh -c.
	#If an array is passed, the "&" operator doesn't work and the command won't return immediatly.
	my $res = system $szCommand;

	$g{log}->is_debug && $g{log}->debug("system call returned $res");
	
	$g{bIsInhibited} = ($res == 0);

	
	return $g{bIsInhibited};
}

sub _uninhibitGnome {
	my $szCommand = $g{prefs}->{szUninhibitCmd};

	if ($szCommand) {
		$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");
		my $res = system $szCommand;
		$g{log}->is_debug && $g{log}->debug("system call returned $res");
		$g{bIsInhibited} = ($res != 0);
	}
	return !$g{bIsInhibited};
}

sub _inhibitOSX {
	my $szCommand = $g{prefs}->{szInhibitCmd};
	
	if (!$szCommand) {
		return 0;
	}
	
	$szCommand = _fixupInhibitCommand( $szCommand );
	
	#redirect output and make it so the command returns immediately..
	$szCommand = $szCommand . " >/dev/null 2>&1 &";

	#execute the system inhibit utility..
	$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");

	my $res = system $szCommand;

	$g{log}->is_debug && $g{log}->debug("system call returned $res");
	
	$g{bIsInhibited} = ($res == 0);

	
	return $g{bIsInhibited};
}

sub _uninhibitOSX {
	my $szCommand = $g{prefs}->{szUninhibitCmd};

	if ($szCommand) {
		$g{log}->is_debug && $g{log}->debug( "Executing command: \"$szCommand\"");
		my $res = system $szCommand;
		$g{log}->is_debug && $g{log}->debug("system call returned $res");
		$g{bIsInhibited} = ($res != 0);
	}
	return !$g{bIsInhibited};
}


sub checkClientActivity {
	my $currenttime = time();

	# Reset the idle countdown counter if 1). scanning, 2). firmware updating or playing, or
	# 3). time-shift (i.e. system time change) or we've resumed from standby or hibernation..
	#my $nIdleTime = $g{prefs}->get('nIdleTime');
	
	if ($g{prefs}->{nIdleTime}) {
		
		#http://bugs.slimdevices.com/show_bug.cgi?id=8520
        if ( _hasResumed($currenttime) || _playersBusy() || Slim::Music::Import->stillScanning() ) {
	
			$g{nHasBeenIdle} = 0;
			$g{log}->is_debug && $g{log}->debug("Resetting idle counter.    " . ($g{prefs}->{nIdleTime} - $g{nHasBeenIdle}) . " minutes left in allowed idle period.");
		}
		
		else {
			
			$g{nHasBeenIdle}++;
			if ($g{nHasBeenIdle} < $g{prefs}->{nIdleTime}) {
				$g{log}->is_debug && $g{log}->debug("Incrementing idle counter. " . ($g{prefs}->{nIdleTime} - $g{nHasBeenIdle}) . " minutes left in allowed idle period.");
			}
		}
	}
	
	
	# If nIdleTime is set to zero in settings, ALWAYS prevent standby..
	# Otherwise, only prevent standby if we're still in the idle time-out period..
	if ( (!$g{prefs}->{nIdleTime}) || $g{nHasBeenIdle} < $g{prefs}->{nIdleTime}) {
		
		#check for bInhibitIsToggle and bIsInhibited..
		if (!$g{prefs}->{bInhibitIsToggle} || ($g{prefs}->{bInhibitIsToggle} && !$g{bIsInhibited}) ) {
			if ($g{szOS} eq 'win') {
				_inhibitWindows();
			} elsif ($g{szOS} eq 'unix') {
				_inhibitGnome();
			} elsif ($g{szOS} eq 'mac') {
				_inhibitOSX();
			}
		}
		
	} else {
		main::INFOLOG && $g{log}->is_info && $g{log}->info("Players have been idle for $g{nHasBeenIdle} minutes. Allowing System Standby...");
		
		if (!$g{bHasRequestedWakeup}) {
			#Make a call to SrvrPowerCtrl to request that the wake alarm be set..no side effects if SrvrPowerCtrl isn't installed..
			my $request = Slim::Control::Request::executeRequest(undef, [ 'srvrpowerctrl', 'setwakealarm' ]);
			$g{bHasRequestedWakeup} = 1;
			$g{log}->is_debug && $g{log}->debug("Wake-alarm set request returned: " . ( defined($request->{_results}) ? Data::Dump::dump($request->{_results}) : "undef" ));
		}

		#Check for bInhibitIsToggle...and bIsInhibited
		if (!$g{prefs}->{bInhibitIsToggle} || ($g{prefs}->{bInhibitIsToggle} && $g{bIsInhibited}) ) {
			if ($g{szOS} eq 'win') {
				_uninhibitWindows();
			} elsif ($g{szOS} eq 'unix') {
				_uninhibitGnome();
			} elsif ($g{szOS} eq 'mac') {
				_uninhibitOSX();
			}
		}
		
	}

	$g{nLastCheckTime} = $currenttime;
	
	Slim::Utils::Timers::killTimers( undef, \&checkClientActivity );
	Slim::Utils::Timers::setTimer(
		undef, 
		time + INTERVAL, 
		\&checkClientActivity
	);

	return 1;
}


sub _playersBusy {
	
	#my $bCheckPower = $g{prefs}->get('bCheckPower');
	my $bCheckPower = $g{prefs}->{bCheckPower};
	
	for my $client (Slim::Player::Client::clients()) {
		
		if ($bCheckPower && $client->power()) {
			$g{log}->is_debug && $g{log}->debug("Player " . $client->name() . " is powered " . ($client->power() ? "on" : "off") . "...");
			return 1;
		}
		
		if ( $client->isUpgrading() || $client->isPlaying() ) {
			$g{log}->is_debug && $g{log}->debug("Player " . $client->name() . " is busy...");
			return 1;
		}
	}
	return 0;
}

sub _hasResumed {
	my $currenttime = shift;

	# We've resumed if the current time is more than INTERVAL + 5 seconds later than the last check time, or
	# if the current time is earlier than the last check time (system time change)
	
	#if ( $currenttime > ($g{nHasBeenIdle} + (INTERVAL * 2)) || $currenttime < $g{nHasBeenIdle} ) {
    if ( $currenttime > ($g{nLastCheckTime} + INTERVAL + 5) || $currenttime < $g{nLastCheckTime} ) {
		
		$g{log}->debug("System has resumed...");
		$g{bHasRequestedWakeup} = 0;
		_killInhibitCmd();

		return 1;
	}
	
	return 0;
}

sub _CLIcheckClientActivity {
	my ($request) = @_;
	my $bIsIdle;

	$request->setStatusProcessing();

	if ( (!$g{prefs}->{nIdleTime}) || $g{nHasBeenIdle} < $g{prefs}->{nIdleTime}) {
		$bIsIdle = 0;
	} else {
		$bIsIdle = 1;
	}
	
	$request->addResult('_isidle', "$bIsIdle");

	$g{log}->is_debug && $g{log}->debug(($bIsIdle ? "Is idle.." : "Not idle.."));
	
	return $request->setStatusDone();

}

sub _pluginCLI {
	my ($request) = @_;
	my $client;
	my $question = lc($request->getParam( '_request'));
	

	logHashElements($request->{_params});

	$client = $request->client();

	# Check that this is a valid request
	if( $request->isNotCommand( [['reallypreventstandby']])) {
		$g{log}->error( "BadDispatch!");
		$request->setStatusBadDispatch();
		return;
	}

	if ($question eq 'isidle'){
		#Request is just asking for confirmation that SrvrPowerCtrl is enabled..
		return _CLIcheckClientActivity($request);
	} else {
		$g{log}->error( "BadParam: $question");
		$request->addResult('_badparam' . "BadParam: $question"  );
		$request->setStatusBadParams();
		return;
		
	}
	
}

sub getIcon {
	my ( $class, $url ) = @_;

	return Plugins::ReallyPreventStandby::Plugin->_pluginDataFor('icon');
}


sub shutdownPlugin {
	Slim::Utils::Timers::killTimers( undef, \&checkClientActivity );
	_killInhibitCmd();

}

1;

__END__
